﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using Scheme.Net.Runtime.AST;

namespace Scheme.Net.Runtime
{
    public class Environment : IEvaluable
    {
        #region Fields

        private Dictionary<Symbol, IEvaluable> env;
        private Environment enclosingEnvironment;
        private int depth;

        private static Environment empty;

        #endregion

        #region Properties

        public bool IsGlobal
        {
            get { return enclosingEnvironment == Environment.Empty; }
        }

        public Environment Parent
        {
            get
            {
                if (IsGlobal)
                    throw new SchemeRuntimeException("Environment has no parent");

                return enclosingEnvironment;
            }
        }

        public int Depth
        {
            get { return depth; }
        }

        public IEnumerable<Symbol> Entries
        {
            get { return env.Keys.OrderBy(s => s.Name); }
        }

        public IEnumerable<Symbol> AllEntries
        {
            get
            {
                IEnumerable<Symbol> l;
                var e = this;
                l = e.Entries;
                while (e.enclosingEnvironment != null)
                {
                    e = e.enclosingEnvironment;
                    l = l.Union(e.Entries);
                }

                return l.OrderBy(s => s.Name);
            }
        }

        #endregion

        #region Construction

        protected Environment()
        {
            env = new Dictionary<Symbol, IEvaluable>();
            depth = 0;
            enclosingEnvironment = null;
        }

        protected Environment(Environment enclosing)
            : this()
        {
            enclosingEnvironment = enclosing;
            depth = enclosingEnvironment.Depth + 1;
        }

        public static Environment Empty
        {
            get
            {
                if (empty == null)
                    empty = new Environment();

                return empty;
            }
        }

        #endregion

        #region Operations

        public IEvaluable Lookup(Symbol var)
        {
            var e = this;
            if (e.env.ContainsKey(var))
            {
                var value = e.env[var];
                if (value == Symbol.Undefined)
                    throw new SchemeRuntimeException(string.Format("Reference to undefined value: {0}", var));
                return value;
            }
            else
            {
                while (e.enclosingEnvironment != null)
                {
                    e = e.enclosingEnvironment;
                    if (e.env.ContainsKey(var))
                    {
                        var value = e.env[var];
                        if (value == Symbol.Undefined)
                            throw new SchemeRuntimeException(string.Format("Reference to undefined value: {0}", var));
                        return value;
                    }
                }

                throw new SchemeRuntimeException(string.Format("Unbound variable: {0}", var));
            }
        }

        public void SetValue(Symbol var, IEvaluable val)
        {
            var e = this;
            if (e.env.ContainsKey(var))
                e.env[var] = val;
            else
            {
                while (e.enclosingEnvironment != null)
                {
                    e = e.enclosingEnvironment;
                    if (e.env.ContainsKey(var))
                    {
                        e.env[var] = val;
                        return;
                    }
                }

                throw new SchemeRuntimeException(string.Format("Unbound variable: {0}", var));
            }
        }

        public void Define(Symbol var, IEvaluable val)
        {
            env[var] = val;
        }

        public Environment Extend(IEnumerable<Symbol> vars, IEnumerable<IEvaluable> vals)
        {
            if (vars.Count() > vals.Count())
                throw new SchemeRuntimeException(
                    string.Format("Too few arguments (vars: {0} / vals: {1}",
                    Pair.CreateList(vars), Pair.CreateList(vals)));
            else if (vars.Count() < vals.Count())
                throw new SchemeRuntimeException(
                    string.Format("Too many arguments (vars: {0} / vals: {1}",
                    Pair.CreateList(vars), Pair.CreateList(vals)));

            var extended = new Environment(this);
            for (var i = 0; i < vars.Count(); i++)
                extended.Define(vars.ElementAt(i), vals.ElementAt(i));

            return extended;
        }

        public Environment Extend()
        {
            return Extend(new Symbol[] { }, new IEvaluable[] { });
        }

        #endregion

        #region Representation

        public override string ToString()
        {
            return string.Format("#<environment (entries = {0}, global? = {1})", env.Keys.Count(), IsGlobal);
        }

        #endregion
    }
}
